package main

import (
	"errors"
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"os"
	"path/filepath"
	"strings"
	"unicode"

	"gitee.com/geektime-geekbang/geektime-go/advance/template/gen/annotation"
	"gitee.com/geektime-geekbang/geektime-go/advance/template/gen/http"
)

// 实际上 main 函数这里要考虑接收参数
// src 源目标
// dst 目标目录
// type src 里面可能有很多类型，那么用户可能需要指定具体的类型
// 这里我们简化操作，只读取当前目录下的数据，并且扫描下面的所有源文件，然后生成代码
// 在当前目录下运行 go install 就将 main 安装成功了，
// 可以在命令行中运行 gen
// 在 testdata 里面运行 gen，则会生成能够通过所有测试的代码
func main() {
	err := gen(".")
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
	fmt.Println("success")
}

func gen(src string) error {
	// 第一步找出符合条件的文件
	srcFiles, err := scanFiles(src)
	if err != nil {
		return err
	}
	// 第二步，AST 解析源代码文件，拿到 service definition 定义
	defs, err := parseFiles(srcFiles)
	if err != nil {
		return err
	}
	// 生成代码
	return genFiles(src, defs)
}

// 根据 defs 来生成代码
// src 是源代码所在目录，在测试里面它是 ./testdata
func genFiles(src string, defs []http.ServiceDefinition) error {
	if defs == nil {
		return errors.New("defs is empty")
	}
	srcAbs, err := filepath.Abs(src)
	if err != nil {
		return err
	}

	for _, sd := range defs {
		fn := underscoreName(sd.Name)
		dstFile := fmt.Sprintf("%s_gen.go", fn)
		dstFileAbs := filepath.Join(srcAbs, dstFile)
		df, err := os.Create(dstFileAbs)
		if err != nil {
			return err
		}
		http.Gen(df, sd)
	}
	return nil
	// panic("implement me")
}

func parseFiles(srcFiles []string) ([]http.ServiceDefinition, error) {
	defs := make([]http.ServiceDefinition, 0, 20)
	for _, src := range srcFiles {
		fmt.Println(src)
		// 你需要利用 annotation 里面的东西来扫描 src，然后生成 file
		// panic("implement me")
		var file annotation.File

		fset := token.NewFileSet()
		f, err := parser.ParseFile(fset, src, nil, parser.ParseComments)
		if err != nil {
			continue
		}
		tv := &annotation.SingleFileEntryVisitor{}

		ast.Walk(tv, f)

		file = tv.Get()

		for _, typ := range file.Types {
			_, ok := typ.Annotations.Get("HttpClient")
			if !ok {
				continue
			}
			def, err := parseServiceDefinition(file.Node.Name.Name, typ)
			if err != nil {
				return nil, err
			}
			defs = append(defs, def)
		}
	}
	return defs, nil
}

// 你需要利用 typ 来构造一个 http.ServiceDefinition
// 注意你可能需要检测用户的定义是否符合你的预期
func parseServiceDefinition(pkg string, typ annotation.Type) (http.ServiceDefinition, error) {
	if pkg == "" {
		return http.ServiceDefinition{}, errors.New("package is empty or missing")
	}

	_, ok := typ.Annotations.Node.Type.(*ast.InterfaceType)
	if !ok {
		return http.ServiceDefinition{}, errors.New("only support interface type")
	}

	if len(typ.Annotations.Node.Type.(*ast.InterfaceType).Methods.List) == 0 {
		return http.ServiceDefinition{}, errors.New("empty interface")
	}

	// Get ServiceName
	snAn, ok := typ.Annotations.Get("ServiceName")
	sn := ""
	if ok {
		sn = snAn.Value
	} else {
		sn = typ.Annotations.Node.Name.Name
	}

	// Get Methods
	methods := typ.Annotations.Node.Type.(*ast.InterfaceType).Methods.List
	// methods := typ.Fields[0].Annotations.Node.Type.(a)
	ms := make([]http.ServiceMethod, 0, len(methods))
	m := http.ServiceMethod{}
	for _, method := range methods {
		// here why Names is slice type
		n := method.Names[0].Name
		m.Name = n

		// here we can check whether the Params/Results is pointer or not
		if _, ok := method.Type.(*ast.FuncType).Params.List[1].Type.(*ast.StarExpr); !ok {
			return http.ServiceDefinition{}, errors.New("request is not pointer")
		}
		m.ReqTypeName = method.Type.(*ast.FuncType).Params.List[1].Type.(*ast.StarExpr).X.(*ast.Ident).Name

		if _, ok := method.Type.(*ast.FuncType).Results.List[0].Type.(*ast.StarExpr); !ok {
			return http.ServiceDefinition{}, errors.New("response is not pointer")
		}
		m.RespTypeName = method.Type.(*ast.FuncType).Results.List[0].Type.(*ast.StarExpr).X.(*ast.Ident).Name

		// Get the Path
		if method.Doc == nil || len(method.Doc.List) == 0 {
			m.Path = fmt.Sprintf("/%s", n)
			ms = append(ms, m)
			continue
		}

		flag := false
		for _, c := range method.Doc.List {
			if e := strings.HasPrefix(c.Text, "// @Path "); e {
				ps := strings.Split(c.Text, "Path ")
				if len(ps) == 2 {
					flag = true
					m.Path = ps[1]
					ms = append(ms, m)
					break
				}
			}
		}
		if !flag {
			m.Path = fmt.Sprintf("/%s", n)
			ms = append(ms, m)
		}
	}

	sd := http.ServiceDefinition{
		Package: pkg,
		Name:    sn,
		Methods: ms,
	}

	return sd, nil
	// panic("implement me")
}

// 返回符合条件的 Go 源代码文件，也就是你要用 AST 来分析这些文件的代码
func scanFiles(src string) ([]string, error) {
	p := fmt.Sprintf("%s/*.go", src)
	match, err := filepath.Glob(p)
	if err != nil {
		return []string{}, err
	}

	res := make([]string, 0, len(match))
	for _, f := range match {
		if strings.HasSuffix(f, "_gen.go") || strings.HasSuffix(f, "_test.go") {
			continue
		}
		fAbs, _ := filepath.Abs(f)
		res = append(res, fAbs)
	}

	return res, nil
	// panic("implement me")
}

// underscoreName 驼峰转字符串命名，在决定生成的文件名的时候需要这个方法
// 可以用正则表达式，然而我写不出来，我是正则渣
func underscoreName(name string) string {
	var buf []byte
	for i, v := range name {
		if unicode.IsUpper(v) {
			if i != 0 {
				buf = append(buf, '_')
			}
			buf = append(buf, byte(unicode.ToLower(v)))
		} else {
			buf = append(buf, byte(v))
		}

	}
	return string(buf)
}
